package com.manlost.web.common.cookie;



import java.text.DateFormat;
import java.text.FieldPosition;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;

/**
 * Tomcat源码摘录
 * 
 * @author kang.zhouk
 *
 */
public class CookieBuilder {

    private static final int                     DEFAULT_VERSION           = 0;

    // Other fields
    private static final String                  OLD_COOKIE_PATTERN        = "EEE, dd-MMM-yyyy HH:mm:ss z";
    private static final ThreadLocal<DateFormat> OLD_COOKIE_FORMAT         = new ThreadLocal<DateFormat>() {

                                                                               protected DateFormat initialValue() {
                                                                                   DateFormat df = new SimpleDateFormat(
                                                                                                                        OLD_COOKIE_PATTERN,
                                                                                                                        Locale.US);
                                                                                   df.setTimeZone(TimeZone.getTimeZone("GMT"));
                                                                                   return df;
                                                                               }
                                                                           };
    private static final String                  ancientDate;

    static {
        ancientDate = ((DateFormat) OLD_COOKIE_FORMAT.get()).format(new Date(10000));
    }

    /**
     * If set to true, we parse cookies according to the servlet spec,
     */
    public static final boolean                  STRICT_SERVLET_COMPLIANCE = false;

    /**
     * If set to false, we don't use the IE6/7 Max-Age/Expires work around
     */
    public static final boolean                  ALWAYS_ADD_EXPIRES        = true;

    // Note: Servlet Spec =< 2.5 only refers to Netscape and RFC2109,
    // not RFC2965

    // Version 1 (RFC2965) attributes
    // TODO Add support for CommentURL
    // Discard - implied by maxAge <0
    // TODO Add support for Port

    public CookieBuilder() {
    }

    // -------------------- utils --------------------

    private static final String tspecials         = ",; ";
    private static final String tspecials2        = "()<>@,;:\\\"/[]?={} \t";
    private static final String tspecials2NoSlash = "()<>@,;:\\\"[]?={} \t";

    /*
     * Tests a string and returns true if the string counts as a reserved token in the Java language.
     * @param value the <code>String</code> to be tested
     * @return <code>true</code> if the <code>String</code> is a reserved token; <code>false</code> if it is not
     */
    private static boolean isToken(String value) {
        return isToken(value, null);
    }

    private static boolean isToken(String value, String literals) {
        String tspecials = (literals == null ? CookieBuilder.tspecials : literals);
        if (value == null) return true;
        int len = value.length();

        for (int i = 0; i < len; i++) {
            char c = value.charAt(i);

            if (tspecials.indexOf(c) != -1) return false;
        }
        return true;
    }

    private static boolean containsCTL(String value, int version) {
        if (value == null) return false;
        int len = value.length();
        for (int i = 0; i < len; i++) {
            char c = value.charAt(i);
            if (c < 0x20 || c >= 0x7f) {
                if (c == 0x09 // allow horizontal tabs
                    || c == 0x0a // allow Line Feed
                    || c == 0x0d) continue; // allow Carriage Return
                return true;
            }
        }
        return false;
    }

    private static boolean isToken2(String value, String literals) {
        String tspecials2 = (literals == null ? CookieBuilder.tspecials2 : literals);
        if (value == null) return true;
        int len = value.length();

        for (int i = 0; i < len; i++) {
            char c = value.charAt(i);

            if (tspecials2.indexOf(c) != -1) return false;
        }
        return true;
    }

    // -------------------- Cookie parsing tools
    /**
     * Return the header name to set the cookie, based on cookie version
     */
    public static String getCookieHeaderName(int version) {
        // TODO Re-enable logging when RFC2965 is implemented
        // log( (version==1) ? "Set-Cookie2" : "Set-Cookie");
        if (version == 1) {
            // XXX RFC2965 not referenced in Servlet Spec
            // Set-Cookie2 is not supported by Netscape 4, 6, IE 3, 5
            // Set-Cookie2 is supported by Lynx and Opera
            // Need to check on later IE and FF releases but for now...
            // RFC2109
            return "Set-Cookie";
            // return "Set-Cookie2";
        } else {
            // Old Netscape
            return "Set-Cookie";
        }
    }

    // TODO RFC2965 fields also need to be passed
   
    public static String getCookieHeaderValue(String name, String value, String path, String domain, int maxAge,
                                              boolean isSecure, boolean isHttpOnly) {
        int version = DEFAULT_VERSION;
        if (!checkName(name)) {
            throw new IllegalArgumentException("Cookie Name could not be " + name + " !!!");
        }
        StringBuffer cookieHeaderBuffer = new StringBuffer();
        // Servlet implementation checks name
        cookieHeaderBuffer.append(name);
        cookieHeaderBuffer.append("=");
        // Servlet implementation does not check anything else

        version = maybeQuote2(version, cookieHeaderBuffer, value);

        // Add version 1 specific information
        if (version == 1) {
            // Version=1 ... required
            cookieHeaderBuffer.append("; Version=1");
        }

        // Add domain information, if present
        if (domain != null) {
            cookieHeaderBuffer.append("; Domain=");
            maybeQuote2(version, cookieHeaderBuffer, domain);
        }

        // Max-Age=secs ... or use old "Expires" format
        // TODO RFC2965 Discard
        if (maxAge >= 0) {
            if (version > 0) {
                cookieHeaderBuffer.append("; Max-Age=");
                cookieHeaderBuffer.append(maxAge);
            }
            // IE6, IE7 and possibly other browsers don't understand Max-Age.
            // They do understand Expires, even with V1 cookies!
            if (version == 0 || ALWAYS_ADD_EXPIRES) {
                // Wdy, DD-Mon-YY HH:MM:SS GMT ( Expires Netscape format )
                cookieHeaderBuffer.append("; Expires=");
                // To expire immediately we need to set the time in past
                if (maxAge == 0) cookieHeaderBuffer.append(ancientDate);
                else ((DateFormat) OLD_COOKIE_FORMAT.get()).format(
                                                                   new Date(System.currentTimeMillis() + maxAge * 1000L),
                                                                   cookieHeaderBuffer, new FieldPosition(0));
            }
        }

        // Path=path
        if (path != null) {
            cookieHeaderBuffer.append("; Path=");
            if (version == 0) {
                maybeQuote2(version, cookieHeaderBuffer, path);
            } else {
                maybeQuote2(version, cookieHeaderBuffer, path, CookieBuilder.tspecials2NoSlash);
            }
        }

        // Secure
        if (isSecure) {
            cookieHeaderBuffer.append("; Secure");
        }

        // HttpOnly
        if (isHttpOnly) {
            cookieHeaderBuffer.append("; HttpOnly");
        }
        return cookieHeaderBuffer.toString();
    }

    private static boolean alreadyQuoted(String value) {
        if (value == null || value.length() == 0) return false;
        return (value.charAt(0) == '\"' && value.charAt(value.length() - 1) == '\"');
    }

    /**
     * Quotes values using rules that vary depending on Cookie version.
     * 
     * @param version
     * @param buf
     * @param value
     */
    private static int maybeQuote2(int version, StringBuffer buf, String value) {
        return maybeQuote2(version, buf, value, null);
    }

    private static int maybeQuote2(int version, StringBuffer cookieHeaderBuffer, String value, String literals) {
        if (value == null || value.length() == 0) {
            cookieHeaderBuffer.append("");
        } else if (containsCTL(value, version)) {
            throw new IllegalArgumentException("Control character in cookie value, consider BASE64 encoding your value");
        } else if (alreadyQuoted(value)) {
            cookieHeaderBuffer.append('"');
            cookieHeaderBuffer.append(escapeDoubleQuotes(value, 1, value.length() - 1));
            cookieHeaderBuffer.append('"');
        } else if (version == 0 && !isToken(value, literals)) {
            cookieHeaderBuffer.append('"');
            cookieHeaderBuffer.append(escapeDoubleQuotes(value, 0, value.length()));
            cookieHeaderBuffer.append('"');
        } else if (version == 1 && !isToken2(value, literals)) {
            cookieHeaderBuffer.append('"');
            cookieHeaderBuffer.append(escapeDoubleQuotes(value, 0, value.length()));
            cookieHeaderBuffer.append('"');
        } else {
            cookieHeaderBuffer.append(value);
        }
        return version;
    }

    /**
     * 对给出的字符串中的双引号进行转义
     * 
     * @param s the input string
     * @param beginIndex start index inclusive
     * @param endIndex exclusive
     * @return The (possibly) escaped string
     */
    private static String escapeDoubleQuotes(String s, int beginIndex, int endIndex) {

        if (s == null || s.length() == 0 || s.indexOf('"') == -1) {
            return s;
        }

        StringBuffer b = new StringBuffer();
        for (int i = beginIndex; i < endIndex; i++) {
            char c = s.charAt(i);
            if (c == '\\') {
                b.append(c);
                // ignore the character after an escape, just append it
                if (++i >= endIndex) throw new IllegalArgumentException("Invalid escape character in cookie value.");
                b.append(s.charAt(i));
            } else if (c == '"') b.append('\\').append('"');
            else b.append(c);
        }

        return b.toString();
    }

    /**
     * 检查CookieName是否合法
     * 
     * @param name
     * @return
     */
    private static boolean checkName(String name) {
        if (!isToken(name) || name.equalsIgnoreCase("Comment") // rfc2019
            || name.equalsIgnoreCase("Discard") // rfc2965
            || name.equalsIgnoreCase("Domain") // rfc2019
            || name.equalsIgnoreCase("Expires") // Netscape
            || name.equalsIgnoreCase("Max-Age") // rfc2019
            || name.equalsIgnoreCase("Path") // rfc2019
            || name.equalsIgnoreCase("Secure") // rfc2019
            || name.equalsIgnoreCase("Version") // rfc2019
        // TODO remaining RFC2965 attributes
        ) {
            return false;
        }
        return true;
    }

}

